﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Text;
using System.Transactions;

using Ordinaire;
using Ordinaire.Commerce;
using Ordinaire.Commerce.Data;
using Ordinaire.Commerce.Db;
using Ordinaire.Social.Data;
using Ordinaire.Taxonomy;

namespace Ordinaire.Commerce
{
    /// <summary>
    /// Represents the custodian that manages all purchases.
    /// </summary>
    public class CommerceManager<TCContext> : BusinessManager<CommerceDataManager<TCContext>, TCContext>
        where TCContext : CContext
    {
        #region Variables

        /// <summary>
        /// Ordinaire.Commerce.GoodCatalogue instance holder.
        /// </summary>
        protected GoodCatalog catalogue;

        /// <summary>
        /// Ordinaire.Taxonomy.Vocabularies instance holder.
        /// </summary>
        protected Vocabularies vocabularies;

        #endregion

        #region Constructor

        /// <summary>
        /// Creates new instance of Ordinaire.Commerce.Purchasing.PurchasingManager.
        /// </summary>
        public CommerceManager()
        {
            base.dataManager = new CommerceDataManager<TCContext>();

            this.vocabularies = new Vocabularies();
            this.ShareContext(vocabularies);

            this.catalogue = new GoodCatalog();
            this.ShareContext(catalogue);
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnitId">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem(int invoiceId, int goodId, int quantity, decimal value, int measureUnitId = 0, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
        {
            return AddInvoiceItem<InvoiceItems<Invoices>, Invoices>(invoiceId, goodId, quantity, value, measureUnitId, tax, discount, invoiceItemId);
        }

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnitId">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem<TInvoiceItems, TInvoices>(int invoiceId, int goodId, int quantity, decimal value, int measureUnitId = 0, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            invoiceId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", invoiceId)));
            goodId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("good id '{0}'", goodId)));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            return HelpAddInvoiceItem<TInvoiceItems, TInvoices>(invoiceId, goodId, quantity, value, measureUnitId, tax, discount, invoiceItemId);
        }

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <param name="invoiceItemId">optionally, the related invoice item id; used for payment/exchange/refund</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem(int invoiceId, string goodCode, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
        {
            return AddInvoiceItem<InvoiceItems<Invoices>, Invoices>(invoiceId, goodCode, quantity, value, measureUnit, tax, discount, invoiceItemId);
        }

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <param name="invoiceItemId">optionally, the related invoice item id; used for payment/exchange/refund</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem<TInvoiceItems, TInvoices>(int invoiceId, string goodCode, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            invoiceId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", invoiceId)));
            goodCode.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "goodCode"));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            Good good = catalogue.GetByCode(goodCode);
            good.ThrowIfNull<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("code '{0}'", goodCode)));

            return HelpAddInvoiceItem<TInvoiceItems, TInvoices>(invoiceId, good.Id, quantity, value, measureUnit.ToVocabId<MeasureUnits>(), tax, discount, invoiceItemId);
        }

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodCode">the good code</param>
        /// <param name="goodName">the good name</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <param name="invoiceItemId">optionally, the related invoice item id; used for payment/exchange/refund</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem(int invoiceId, string goodCode, string goodName, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
        {
            return AddInvoiceItem<InvoiceItems<Invoices>, Invoices>(invoiceId, goodCode, goodName, quantity, value, measureUnit, tax, discount, invoiceItemId);
        }

        /// <summary>
        /// Adds new invoice item into the specified invoice id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceId">the purchase id where the item is added to</param>
        /// <param name="goodCode">the good code</param>
        /// <param name="goodName">the good name</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total item value</param>
        /// <param name="measureUnit">optionally, the unit of measure applicable for the specified quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount given</param>
        /// <param name="invoiceItemId">optionally, the related invoice item id; used for payment/exchange/refund</param>
        /// <returns>the reference id of the new invoice item</returns>
        public int AddInvoiceItem<TInvoiceItems, TInvoices>(int invoiceId, string goodCode, string goodName, int quantity, decimal value, string measureUnit = null, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            invoiceId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("purchase id '{0}'", invoiceId)));
            goodName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "goodName"));
            quantity.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("quantity '{0}'", quantity)));

            // Retrieve or add good into catalog
            int goodId = goodCode.ToGoodIdByCode(goodName, measureUnit, CommerceCalculator.ValuePerUnit(value, quantity));

            // Add good into purchase
            int purchaseGoodId = HelpAddInvoiceItem<InvoiceItems<Invoices>, Invoices>(invoiceId, goodId, quantity, value, measureUnit.ToVocabId<MeasureUnits>(), tax, discount, invoiceItemId);
            return purchaseGoodId;
        }

        /// <summary>
        /// Deletes invoice item with specified id.
        /// </summary>
        /// <param name="invoiceItemId">the InvoiceItems id</param>
        /// <returns>true if delete is successful; otherwise false</returns>
        public bool DeleteInvoiceItem(int invoiceItemId)
        {
            return DeleteInvoiceItem<InvoiceItems<Invoices>, Invoices>(invoiceItemId);
        }

        /// <summary>
        /// Deletes invoice item with specified id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceItemId">the InvoiceItems id</param>
        /// <returns>true if delete is successful; otherwise false</returns>
        public bool DeleteInvoiceItem<TInvoiceItems, TInvoices>(int invoiceItemId)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            invoiceItemId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("invoice item id '{0}'", invoiceItemId)));
            return HelpDeleteInvoiceItem<TInvoiceItems, TInvoices>(invoiceItemId);
        }

        /// <summary>
        /// Returns invoice with the specified id.
        /// </summary>
        /// <param name="id">invoice id</param>
        /// <returns>invoice with the specified id</returns>
        public Invoice GetInvoice(int id)
        {
            return GetInvoice<Invoices>(id);
        }

        /// <summary>
        /// Returns invoice with the specified id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <param name="id">invoice id</param>
        /// <returns>invoice with the specified id</returns>
        public Invoice GetInvoice<TInvoices>(int id)
            where TInvoices : Invoices
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, String.Format("invoice id '{0}'", id)));

            TInvoices invoiceData = dataManager.Get<TInvoices>(id);
            if (invoiceData == null)
            {
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.Invalid, String.Format("invoice id '{0}'", id));
            }

            return (Invoice) invoiceData.ToInvoice();
        }

        /// <summary>
        /// Creates new invoice into default "Invoices" table.
        /// </summary>
        /// <param name="date">the invoice date</param>
        /// <param name="organisationId">the organisation id where the goods was purchased from</param>
        /// <param name="personId">the person id whom the goods was purchased from</param>
        /// <param name="serialNo">optional purchase serial number</param>
        /// <returns>the reference id of the new invoice</returns>
        public int NewInvoice(DateTime? date = null, int organisationId = 0, int personId = 0, string serialNo = "")
        {
            return NewInvoice<Invoices>(date, organisationId, personId, serialNo);
        }

        /// <summary>
        /// Creates new invoice number in the data source.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <param name="date">the invoice date</param>
        /// <param name="organisationId">the organisation id where the goods was purchased from</param>
        /// <param name="personId">the person id whom the goods was purchased from</param>
        /// <param name="serialNo">optional purchase serial number</param>
        /// <returns>the reference id of the new invoice</returns>
        public int NewInvoice<TInvoices>(DateTime? date = null, int organisationId = 0, int personId = 0, string serialNo = "")
            where TInvoices : Invoices
        {
            return HelpNewInvoice<TInvoices>(date, organisationId, personId, serialNo);
        }

        #endregion

        #region Protected methods

        /// <summary>
        /// Returns new invoice number in "IyyMMddhhmmssf" format (eg. I1303111141301)
        /// </summary>
        /// <returns>the invoice number</returns>
        protected virtual string GenerateSerialNo()
        {
            return DateTime.Now.ToString("IyyMMddHHmmssf");
        }

        /// <summary>
        /// Serves as a helper function to add new invoice item into the specified invoice id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceId">the purchase id where the purchase item is stored to</param>
        /// <param name="goodId">the good id</param>
        /// <param name="quantity">the quantity of the good</param>
        /// <param name="value">the total purchase value</param>
        /// <param name="measureUnitId">optionally, the unit of measure applicable for the quantity</param>
        /// <param name="tax">optionally, the tax value imposed</param>
        /// <param name="discount">optionally, the discount value given</param>
        /// <param name="invoiceItemId">optionally, the related invoice item id; used for payment/exchange/product</param>
        /// <returns>the reference id of the purchase item</returns>
        protected int HelpAddInvoiceItem<TInvoiceItems, TInvoices>(int invoiceId, int goodId, int quantity, decimal value, int measureUnitId = 0, decimal tax = 0, decimal discount = 0, int? invoiceItemId = null)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            // new record is created everytime this function is called
            // TODO: is it necessary to update purchased good quantity, if value per unit is the same?
            TInvoiceItems newData  = typeof(TInvoiceItems).Instance<TInvoiceItems>();
            newData.ForeignId      = invoiceId;
            newData.GoodId         = goodId;
            newData.InvoiceItemId  = invoiceItemId;
            newData.Quantity       = quantity;
            newData.MeasureUnitId  = (measureUnitId <= 0) ? (int?) null : measureUnitId;
            newData.Value          = value;
            newData.Tax            = tax;
            newData.Discount       = discount;

            dataManager.Insert<TInvoiceItems, int, string>(newData);
            return newData.Id;
        }

        /// <summary>
        /// Serves as a helper function to delete specified InvoiceItems id.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <typeparam name="TInvoiceItems">other "InvoiceItems" table to use</typeparam>
        /// <param name="invoiceItemId">the InvoiceItems id</param>
        /// <returns>true if delete is successful; otherwise false</returns>
        protected bool HelpDeleteInvoiceItem<TInvoiceItems, TInvoices>(int invoiceItemId)
            where TInvoices : Invoices
            where TInvoiceItems : InvoiceItems<TInvoices>
        {
            TInvoiceItems dataToDelete = dataManager.Get<TInvoiceItems, int, string>(invoiceItemId);
            if (dataToDelete == null)
            {
                return false;
            }

            Context.GetTable<TInvoiceItems>().DeleteOnSubmit(dataToDelete);
            Context.SubmitChanges();
            return true;
        }

        /// <summary>
        /// Serves as a helper function to create new invoice in the data source.
        /// </summary>
        /// <typeparam name="TInvoices">other "Invoices" table to use</typeparam>
        /// <param name="date">the invoice date</param>
        /// <param name="organisationId">the organisation id where the goods was purchased from</param>
        /// <param name="personId">the person id whom the goods was purchased from</param>
        /// <param name="serialNo">optional purchase serial number</param>
        /// <returns>the reference id of the new invoice</returns>
        protected int HelpNewInvoice<TInvoices>(DateTime? date = null, int organisationId = 0, int personId = 0, string serialNo = "")
            where TInvoices : Invoices
        {
            // use today's date if not specified
            date = date ?? DateTime.Today;

            if (String.IsNullOrEmpty(serialNo))
            {
                serialNo = GenerateSerialNo();
            }

            var data = dataManager.InsertInvoices<TInvoices>(date.Value, organisationId, personId, serialNo);
            if (data == null)
            {
                return 0;
            }
            return data.Id;
        }

        #endregion
    }
}
